/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.veditor.model;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import javax.jcr.PropertyType;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.html.HTML;
import offset.nodes.Constants;
import offset.nodes.client.editor.model.ComponentModel;
import offset.nodes.client.editor.model.XmlUtils;
import offset.nodes.client.model.NodeTypes;
import offset.nodes.client.model.SimpleNamespaceRegistry;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeTypeDefinition;
import offset.nodes.client.virtual.model.jcr.nodetype.PropertyDefinition;

/**
 * Creates the transformation style sheet from the sample HTML
 * 
 * @author Walter Lütgenau
 */
public class StyleSheetWriter {

    Element styleSheet;
    ArrayList templates = new ArrayList();
    HashSet templateNames = new HashSet();
    Namespaces namespaces = new Namespaces();
    public static final String NAME_BASIC_STYLESHEET = "template-basic.xsl";
    boolean editorMode = false;
    DataModel model;
    BrowserElementActionModel elementActionModel;

    public StyleSheetWriter(Element styleSheet, DataModel model) throws BadLocationException {
        this(styleSheet, false, model);
        this.model = model;
    }

    /**
     * Creates the transformation templates for the style sheet element.
     * 
     * @param styleSheet
     * @param editorMode
     * @param model
     * @throws BadLocationException
     */
    public StyleSheetWriter(Element styleSheet, boolean editorMode, DataModel model) throws BadLocationException {
        this.model = model;
        this.elementActionModel = model.getBrowserElementActionModel();
        this.styleSheet = styleSheet;
        this.editorMode = editorMode;
        int level = 1;
        if (model.isData(styleSheet)) {
            templates.add(println(level, "<xsl:template match=\"/\" ><xsl:apply-templates select=\"" + model.getData(styleSheet) + "\"/></xsl:template>"));
            String path = toPath(model.getData(styleSheet));
            addTemplate(createName(path), styleSheet, path);
        }
        else {
            StringBuilder buf = new StringBuilder();
            buf.append(println(level, "<xsl:template match=\"/\">"));
            buf.append(addChildren(styleSheet, level + 1, ""));
            buf.append(println(level, "</xsl:template>"));
            templates.add(buf.toString());
        }
    }

    public Element getStyleSheet() {
        return styleSheet;
    }

    public ArrayList getTemplates() {
        return templates;
    }

    public HashSet getTemplateNames() {
        return templateNames;
    }

    public Namespaces getNamespaces() {
        return namespaces;
    }

    public boolean isEditorMode() {
        return editorMode;
    }

    public DataModel getModel() {
        return model;
    }

    class Namespaces {

        public Namespaces() {
            prefixes.add("jcr");
        }
        HashSet prefixes = new HashSet();

        public void register(String qName) {
            String[] names = qName.split("/");
            for (int i = 0; i < names.length; i++) {
                if (names[i].startsWith("@"))
                    names[i] = names[i].substring(1);
                int end = -1;
                if ((end = names[i].indexOf(":")) < 0)
                    continue;
                prefixes.add(names[i].substring(0, end));
            }
        }

        public String write() {
            StringBuilder buf = new StringBuilder();
            SimpleNamespaceRegistry namespaces = SimpleNamespaceRegistry.getInstance();
            Iterator i = prefixes.iterator();
            while (i.hasNext()) {
                String prefix = (String) i.next();
                String uri = namespaces.getURI(prefix);
                if (uri == null)
                    uri = "http://anywhere/anything";
                buf.append(" xmlns:" + prefix + "=\"" + uri + "\"");
            }
            buf.append(" xmlns:repo=\"xalan://offset.nodes.server.model.RepositoryUtils\"");
            return buf.toString();
        }
    }

    protected boolean hasDataChildren(Element e) {
        for (int i = 0; i < e.getElementCount(); i++) {
            if (model.isData(e.getElement(i)))
                return true;
        }
        return false;
    }

    protected String print(int level, String msg) {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < level; i++) {
            buf.append("    ");
        }
        buf.append(msg);
        return buf.toString();
    }

    protected String println(int level, String msg) {
        return print(level, msg) + "\n";
    }

    /**
     * Add a leaf element to the buffer
     *
     * @param buf
     * @param level
     * @param child
     * @throws BadLocationException
     */
    protected void addLeaf(StringBuilder buf, int level, Element child, int propertyType) throws BadLocationException {
        buf.insert(0, println(level, "<!--" + toText(model.getContent(child)) + "-->"));
        
        String templateName = "value-of";
        if (propertyType == PropertyType.REFERENCE)
            templateName = "hyperlink-of";
        
        buf.append(println(level, "<xsl:call-template name=\"" + templateName + "\">"));
        buf.append(println(level + 1, "<xsl:with-param name=\"input\" select=\".\"/>"));
        buf.append(println(level, "</xsl:call-template>"));
    }

    /**
     * Return the style sheet part associated to the given element's children
     * 
     * @param e
     * @param level
     * @param currentElementPath
     * @return
     * @throws BadLocationException
     */
    protected String addChildren(Element e, int level, String currentElementPath) throws BadLocationException {
        if (e.getName().equals(HTML.Tag.IMPLIED.toString()))
            level--;
        StringBuilder buf = new StringBuilder();
        String lastDataName = null;
        for (int i = 0; i < e.getElementCount(); i++) {
            Element child = e.getElement(i);
            if (child.getName().equals(HTML.Tag.CONTENT.toString())) {
                // simple content: check for constant parts
                if (!isValidContent(child))
                    continue;
                boolean childrenHaveData = hasDataChildren(e);
                boolean parentHasData = model.isData(e) || (e.getName().equals(HTML.Tag.IMPLIED.toString()) && model.isData(e.getParentElement()));
                if (parentHasData && (!childrenHaveData || model.isData(child)))
                    // this is a leaf data element
                    addLeaf(buf, level, child, getModel().getPropertyType(currentElementPath));
                else if (!isInlineData(child))
                    buf.append(println(level, toText(model.getContent(child))));
                else {
                    // create inline template for data element
                    String dataName = getInlineData(child);
                    if (lastDataName != null && lastDataName.equals(dataName))
                        continue;
                    
                    buf.append(buildInlineDataElement(child, level, currentElementPath, dataName));
                    lastDataName = dataName;
                }
            }
            else if (model.isData(child)) {
                // create template for data element
                String dataName = model.getData(child);

                if (hasDescendant(child, dataName)) {
                    buf.append(buildConditionElement(child, level, currentElementPath, dataName));
                    continue;
                }

                if (lastDataName != null && lastDataName.equals(dataName))
                    continue;
                buf.append(buildDataElement(child, level, currentElementPath, dataName));
                lastDataName = dataName;
                if (child.getName().equals(HTML.Tag.SPAN.toString()))
                    i += 2;
            }
            else if (child.getName().equals(HTML.Tag.IMPLIED.toString())) {
                // this element is artificial and shall not be included in the transformation
                buf.append(addChildren(child, level + 1, currentElementPath));
                lastDataName = null;
            }
            else if (editorMode && isIconImage(child)) {
                // icon image: same as leaf element
                addLeaf(buf, level, child, -1);
                lastDataName = null;
            }
            else {
                buf.append(buildNonDataElement(child, level, currentElementPath));
                lastDataName = null;
            }
        }
        return buf.toString();
    }

    /**
     * Create the transformation part for an element with a n-data attribute
     * 
     * @param child
     * @param level
     * @param currentElementPath
     * @param dataName
     * @return
     * @throws BadLocationException
     */
    protected String buildDataElement(Element child, int level, String currentElementPath, String dataName) throws BadLocationException {
        String elementPath = null;
        String selectPath = null;

        if (model.isSecondaryQueryRoot(dataName)) {
            elementPath = dataName;
            selectPath = elementPath;
        } else {
            elementPath = currentElementPath + "/" + lastPathComponent(dataName);
            selectPath = lastPathComponent(elementPath);
        }
        namespaces.register(elementPath);
        String templateName = createName(elementPath);
        addTemplate(templateName, child, elementPath);

        return buildTemplateCallingCode(child, level, selectPath, templateName);
    }
    
    /**
     * Build the style sheet code, that calls the template for the element associated to the data.
     * 
     * @param child the element, that is associated to data
     * @param level the indent level
     * @param selectPath the path used to select the element in the style sheet
     * @param templateName the name of the template
     * @return the code
     */
    protected String buildTemplateCallingCode(Element child, int level, String selectPath, String templateName) {
        if (insertEmptyChildren(child))
            return buildEmptyChildrenTemplateCallingCode(child, level, selectPath, templateName);
        
        StringBuilder buf = new StringBuilder();
        
        buf.append(println(level, "<xsl:for-each select=\"" + selectPath + "\">"));
        buf.append(println(level + 1, "<xsl:call-template name=\"" + templateName + "\"/>"));
        buf.append(println(level, "</xsl:for-each>"));
        
        return buf.toString();
    }
    
    /**
     * Build the style sheet code, that calls the template for the element associated to the data
     * in case the element shall be inserted even if empty.
     * 
     * This is for example true, if the element is a TD. In that case, an empty TD shall be inserted if there is no data.
     * 
     * @param child the element associated to the data
     * @param level the indent level
     * @param selectPath the path used to select the element in the style sheet
     * @param templateName the name of the template
     * @return the code
     */
    protected String buildEmptyChildrenTemplateCallingCode(Element child, int level, String selectPath, String templateName) {
        StringBuilder buf = new StringBuilder();
        
        buf.append(println(level, "<xsl:choose>"));
        buf.append(println(level + 1, "<xsl:when test=\"" + selectPath + "\">"));
        buf.append(println(level + 2, "<xsl:for-each select=\"" + selectPath + "\">"));
        buf.append(println(level + 3, "<xsl:call-template name=\"" + templateName + "\"/>"));
        buf.append(println(level + 2, "</xsl:for-each>"));
        buf.append(println(level + 1, "</xsl:when>"));
        buf.append(println(level + 1, "<xsl:otherwise>"));
        buf.append(println(level + 2, "<xsl:call-template name=\"" + templateName + "\"/>"));
        buf.append(println(level + 1, "</xsl:otherwise>"));
        buf.append(println(level, "</xsl:choose>"));
        
        return buf.toString();
    }
    
    /**
     * Create the transformation part for an inline element with a n-data attribute
     * 
     * @param child
     * @param level
     * @param currentElementPath
     * @param dataName
     * @return
     * @throws BadLocationException
     */    
    protected String buildInlineDataElement(Element child, int level, String currentElementPath, String dataName) throws BadLocationException {
        StringBuilder buf = new StringBuilder();
        
        String path = currentElementPath + "/" + lastPathComponent(dataName);
        if (model.isSecondaryQueryRoot(dataName)) {
            path = dataName;
        }
        namespaces.register(path);
        String name = createName(path);
        buf.append(println(level, "<xsl:for-each select=\"" + lastPathComponent(dataName) + "\">"));
        buf.append(println(level + 1, "<xsl:call-template name=\"" + name + "\"/>"));
        buf.append(println(level, "</xsl:for-each>"));
        addInlineTemplate(name, child, path);
        
        return buf.toString();        
    }

    /**
     * Create the transformation part for an element with a n-data attribute, which has a descendant with the same attribute value.
     * This signals, that the user wanted to have all code below the element only conditionally, if the particular data element is present.
     * 
     * @param child
     * @param level
     * @param currentElementPath
     * @param dataName
     * @return
     * @throws BadLocationException
     */
    protected String buildConditionElement(Element child, int level, String currentElementPath, String dataName) throws BadLocationException {
        StringBuilder buf = new StringBuilder();

        buf.append(println(level, "<xsl:if test=\"" + lastPathComponent(dataName) + "\">"));

        SimpleAttributeSet atts = new SimpleAttributeSet(child.getAttributes());
        atts.removeAttribute(DataModel.ATTRIBUTE_NAME_DATA);
        buf.append(println(level, model.getStartTag(atts)));
        buf.append(addChildren(child, level + 1, currentElementPath));
        buf.append(println(level, model.getEndTag(child)));

        buf.append(println(level, "</xsl:if>"));

        return buf.toString();
    }

    /**
     * Create the transformation part for an element without a n-data attribute
     *
     * @param child
     * @param level
     * @param currentElementPath
     * @return
     * @throws BadLocationException
     */
    protected String buildNonDataElement(Element child, int level, String currentElementPath) throws BadLocationException {
        StringBuilder buf = new StringBuilder();
        // simply copy element to output
        buf.append(getStartTag(level, child));
        buf.append(addChildren(child, level + 1, currentElementPath));
        buf.append(println(level, model.getEndTag(child)));

        return buf.toString();
    }
    
    /**
     * Return the start element tag as a string.
     * 
     * If the element is marked as an element action, substitute it with the template invocation.
     * 
     * @param level the indent level
     * @param e the element
     * @return the string representing the start element tag
     */
    public String getStartTag(int level, Element e) {
        if (elementActionModel.getElementActionName(e) == null)
            return model.getStartTag(e);
        
        String elementActionName = elementActionModel.getElementActionName(e);
        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());
        atts.removeAttribute(BrowserElementActionModel.ATTRIBUTE_NAME_ELEMENT_ACTION);
        
        StringBuilder buf = new StringBuilder();
        buf.append(println(level, model.getStartTag(atts)));
        buf.append(println(level + 1, "<xsl:call-template name=\"invokeAction\">"));
        buf.append(println(level + 2, "<xsl:with-param name=\"actionName\">" + elementActionName + "</xsl:with-param>"));
        buf.append(println(level + 1, "</xsl:call-template>"));

        return buf.toString();
    }

    protected boolean hasDescendant(Element e, String dataName) {
        for (int i = 0; i < e.getElementCount(); i++) {
            Element child = e.getElement(i);

            if (model.isData(child) && model.getData(child).equals(dataName))
                return true;

            if (hasDescendant(child, dataName))
                return true;
        }

        return false;
    }

    protected String toNamePart(String namePart) {
        if (namePart.length() == 0)
            return namePart;
        int index = -1;
        if (namePart.charAt(0) == '@')
            namePart = namePart.substring(1);
        namePart = namePart.replaceAll(":", "_");
        if ((index = namePart.indexOf("[")) >= 0)
            namePart = namePart.substring(0, index);
        return namePart;
    }

    protected String createName(String path) {
        String name = toName(path);
        if (templateNames.contains(name)) {
            int i = 2;
            while (templateNames.contains(name + "." + i)) {
                i++;
            }
            name = name + "." + i;
        }
        return name;
    }

    protected boolean isIconImage(Element e) {
        if (!e.getName().equals(HTML.Tag.IMG.toString()))
            return false;
        String src = (String) e.getAttributes().getAttribute(HTML.Attribute.SRC);
        if (src == null)
            return false;
        String key = Constants.PAR_CONTENT_TYPE + "=" + Constants.CONTENT_TYPE_ICON;
        if (src.indexOf(key) < 0)
            return false;
        return true;
    }

    protected String toName(String path) {
        String[] nameParts = path.split("/");
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < nameParts.length; i++) {
            if (buf.length() > 0)
                buf.append(".");
            buf.append(toNamePart(nameParts[i]));
        }
        return buf.toString();
    }

    protected String toPath(String query) {
        String[] nameParts = query.split("/");
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < nameParts.length; i++) {
            if (i > 0)
                buf.append("/");
            int index = -1;
            if ((index = nameParts[i].indexOf("[")) >= 0)
                nameParts[i] = nameParts[i].substring(0, index);
            buf.append(nameParts[i]);
        }
        return buf.toString();
    }

    protected String toText(String msg) {
        return XmlUtils.attributeValueToXML(msg);
    }

    protected String lastPathComponent(String path) {
        return path.substring(path.lastIndexOf('/') + 1);
    }

    // is content and not last content element (i.removable. the one with only the linefeed)
    protected boolean isValidContent(Element e) {
        if (!e.getName().equals(HTML.Tag.CONTENT.toString()))
            return false;
        if (e.getEndOffset() - e.getStartOffset() > 1)
            return true;
        if (e.getParentElement().getElementCount() != model.getChildIndex(e) + 1)
            return true;
        return false;
    }
    
    /**
     * For certain elements, it is necessary to add even empty child elements.
     * 
     * "Empty" means child elements associated to optional data, which are not in the repository.
     * 
     * @param e the element associated to the data
     * @return 
     */
    protected boolean insertEmptyChildren(Element e) {
        return e.getParentElement().getName().equalsIgnoreCase("tr");
    }

    /**
     * Add the template for an ordinary element with a n-data attribute.
     * 
     * @param name
     * @param template
     * @param path
     * @throws BadLocationException
     */
    protected void addTemplate(String name, Element template, String path) throws BadLocationException {
        int level = 1;
        StringBuilder buf = new StringBuilder();
        buf.append(println(level, "<xsl:template name=\"" + name + "\" match=\"" + path + "\">"));
        if (template.getAttributes().isDefined(model.ATTRIBUTE_NAME_BINARY))
            // external data from a component
            addBinaryComponent(buf, level + 1, template);
        else if (template.getAttributes().isDefined(ComponentModel.ATT_COMPONENT_ID))
            addComponent(buf, level + 1, template);
        else if (template.getAttributes().isDefined(model.ATTRIBUTE_NAME_PAGE))
            addPage(buf, level + 1, template);
        else {
            buf.append(println(level + 1, buildStartTag(template)));
            buf.append(addChildren(template, level + 2, path));
            buf.append(println(level + 1, model.getEndTag(template)));
        }
        buf.append(println(level, "</xsl:template>"));
        templates.add(buf.toString());
        templateNames.add(name);
    }

    /**
     * Add the content of an external page referenced by the current element to the output.
     * 
     * The content of the page is the element of the HTML page that has id "content".
     * The implementation uses the template "writePageContent" which in turn
     * uses a java extension to retrieve the content.
     *
     * @param buf
     * @param level
     * @param template
     */
    protected void addPage(StringBuilder buf, int level, Element template) {
        buf.append(println(level, model.getStartTag(template)));
        buf.append(println(level + 1, "<xsl:call-template name=\"writePageContent\"/>"));
        buf.append(println(level, model.getEndTag(template)));
    }

    /**
     * Add the transformation for a binary property, which is assumed to have been written by a component editor.
     * 
     * @param buf
     * @param level
     * @param template
     */
    protected void addBinaryComponent(StringBuilder buf, int level, Element template) {
        SimpleAttributeSet atts = new SimpleAttributeSet(template.getAttributes());
        atts.removeAttribute(StyleConstants.NameAttribute);
        atts.addAttribute(StyleConstants.NameAttribute, HTML.Tag.DIV);
        buf.append(println(level, model.getStartTag(atts)));
        buf.append(println(level + 1, "<xsl:value-of disable-output-escaping=\"yes\" select=\".\"/>"));
        buf.append(println(level, model.getEndTag(atts)));
    }

    /**
     * Add the transformation for an arbitrary node, which is assumed to have been written by a component editor.
     * 
     * @param buf
     * @param level
     * @param template
     */
    protected void addComponent(StringBuilder buf, int level, Element template) {
        SimpleAttributeSet atts = new SimpleAttributeSet(template.getAttributes());
        atts.removeAttribute(StyleConstants.NameAttribute);
        atts.addAttribute(StyleConstants.NameAttribute, HTML.Tag.DIV);
        buf.append(println(level, model.getStartTag(atts)));
        if (!editorMode)
            buf.append(println(level + 1, "<xsl:call-template name=\"writePageContent\"/>"));
        buf.append(println(level, model.getEndTag(atts)));
    }

    /**
     * Add the template for an inline element with a n-data attribute
     *
     * @param name
     * @param template
     * @param path
     * @throws BadLocationException
     */
    protected void addInlineTemplate(String name, Element template, String path) throws BadLocationException {
        int level = 1;
        StringBuilder buf = new StringBuilder();
        buf.append(println(level, "<xsl:template name=\"" + name + "\" match=\"" + path + "\">"));
        buf.append(println(level, "<!--" + toText(model.getContent(template)) + "-->"));
        buf.append(println(level + 1, buildInlineStartTag(template)));
        buf.append(println(level, "<xsl:call-template name=\"value-of\">"));
        buf.append(println(level + 1, "<xsl:with-param name=\"input\" select=\".\"/>"));
        buf.append(println(level, "</xsl:call-template>"));
        buf.append(println(level + 1, "</" + getInlineName(template) + ">"));
        buf.append(println(level, "</xsl:template>"));
        templates.add(buf.toString());
        templateNames.add(name);
    }

    protected HTML.Tag getInlineName(Element e) {
        Enumeration names = e.getAttributes().getAttributeNames();
        while (names.hasMoreElements()) {
            Object name = names.nextElement();
            if (name instanceof HTML.Tag)
                return (HTML.Tag) name;
        }
        return null;
    }

    protected boolean isInlineData(Element e) {
        if (e.getAttributes().getAttributeCount() <= 1)
            return false;
        if (getInlineName(e) == null)
            return false;
        return true;
    }

    protected String getInlineData(Element e) {
        AttributeSet atts = (AttributeSet) e.getAttributes().getAttribute(getInlineName(e));
        return (String) atts.getAttribute(model.ATTRIBUTE_NAME_DATA);
    }

    protected String buildStartTag(Element e) {
        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());
        atts.addAttribute(model.ATTRIBUTE_NAME_ID, "{@jcr:uuid}");
        return model.getStartTag(atts);
    }

    protected String buildInlineStartTag(Element e) {
        SimpleAttributeSet atts = new SimpleAttributeSet();
        atts.addAttribute(StyleConstants.NameAttribute, getInlineName(e));
        AttributeSet inner = (AttributeSet) e.getAttributes().getAttribute(getInlineName(e));
        Enumeration names = inner.getAttributeNames();
        while (names.hasMoreElements()) {
            Object name = names.nextElement();
            String value = (String) inner.getAttribute(name);
            atts.addAttribute(name, value);
        }
        return model.getStartTag(atts);
    }

    /**
     * The only public method, which writes the templates to the printstream.
     * 
     * @param out
     */
    public void write(PrintStream out) {
        out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        out.println("<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"");
        out.println(namespaces.write() + ">");
        out.println(println(1, "<xsl:import href=\"" + NAME_BASIC_STYLESHEET + "\"/>"));
        out.println(println(1, "<xsl:output method=\"html\"/>"));
        out.println();
        
        Iterator i = templates.iterator();
        while (i.hasNext()) {
            String template = (String) i.next();
            out.println(template);
        }
        
        out.println("</xsl:stylesheet>");
    }

}
